<template>
  <div id="ws-panel" class="panel">
    <!-- 连接websocket -->
    <div v-if="!websocket" class="ws-connect">
      <input type="text" ref="portIpt" @keyup="e => e.key == 'Enter' && connect()" placeholder="websocket端口">
      <button @click="connect(this.value)">连接</button>
    </div>

    <!-- 断联 -->
    <div v-if="websocket" style="display: flex">
      <button @click="disconnect" style="flex: 0 0 auto;">断开连接</button>
      <input type="text" placeholder="图片地址" :value="imgPath" style="flex: 1 1 auto; min-width: 0;">
      <button @click="() => {
        // ipt_getImgPath.click()

        wss({
          req: 'select-file'
        })
      }" style="flex: 0 0 auto;">选择图片</button>
      <input ref="ipt_getImgPath" type="file" accept="image/*" style="display: none;" @input="e => {
        const ipt = e.target.value
        if (ipt.length) {
          imgPath = ipt

          localStorage.setItem('lastImgPath', ipt)
        }
      }">
      <span v-if="img_metadata">{{ img_metadata.width }}x{{ img_metadata.height }}</span>
      <span v-if="img_metadata">{{ img_metadata.channels }} 通道 {{ img_metadata.space.toUpperCase() }}</span>
    </div>

    <div>
      <span>K-means聚类数量: <input type="number" :value="calc_conf.k" @input="e => {
        calc_conf.k = Number(e.target.value)

      }" min="2" step="1">
      </span>
      <span>最小像素: <input type="number" :value="calc_conf.min_px" @input="e => {
        calc_conf.min_px = e.target.value
      }" min="1" step="1">
      </span>
      <span>阈值: <input type="number" :value="calc_conf.color_thresh" @input="e => {
        calc_conf.color_thresh = e.target.value
      }" min="2" step="1">
      </span>
      <button @click="() => {
        wss({ req: 'calc-split', path: imgPath, k: calc_conf.k, color_thresh: calc_conf.color_thresh, min_px: calc_conf.min_px, token: Date.now() })
      }">拆分图像</button>
    </div>
  </div>

  <div v-if="msg_ret" id="edit-wrapper" class="panel">
    <img :src="img_ret" alt="">
    <ul id="edit-body">
      <li>
        <h5>所有颜色</h5>
        <div>
          <i>H</i>
          <input type="text" min="0" max="360" @input="elem => {
            const temp = elem.target.value

            for (let k in msg_ret) {
              msg_ret[k].offset.H = temp

              msg_ret[k].msg = `H: ${msg_ret[k].offset.H}, S: ${msg_ret[k].offset.S}, L: ${msg_ret[k].offset.L}`
            }

          }"></input>
          <i>S</i>
          <input type="text" min="0" max="100" @input="elem => {
            const temp = elem.target.value
            for (let k in msg_ret) {
              msg_ret[k].offset.S = temp

              msg_ret[k].msg = `H: ${msg_ret[k].offset.H}, S: ${msg_ret[k].offset.S}, L: ${msg_ret[k].offset.L}`
            }

          }"></input>
          <i>L</i>
          <input type="text" min="0" max="100" @input="elem => {
            const temp = elem.target.value
            for (let k in msg_ret) {
              msg_ret[k].offset.L = temp

              msg_ret[k].msg = `H: ${msg_ret[k].offset.H}, S: ${msg_ret[k].offset.S}, L: ${msg_ret[k].offset.L}`
            }

          }"></input>

          <button @click="() => {

            img_ret = []
            join(msg_ret)
          }">应用更改</button>
        </div>

      </li>
      <li v-for="obj, key in msg_ret">
        <span class="color-block"
          :style="`background: hsl(${obj.split_color.H + obj.offset.H}, ${obj.split_color.S + obj.offset.S}%, ${obj.split_color.L + obj.offset.L}%)`"></span>

        <div>
          <h5>{{ key }}</h5>
          <div>
            <i>HSL</i>
            <input type="text" placeholder="H,S,L" @input="elem => {
              let temp = elem.target.value.split(',')

              msg_ret[key].msg = ''

              let h = 0,
                s = 0,
                l = 0

              try {
                console.log(temp);

                if (temp.length == 1) {
                  if (Number(temp[0]) != temp[0] || temp[0] > 100) throw ''
                  h = s = l = temp[0]
                } else if (temp.length == 2) {
                  if (Number(temp[0]) != temp[0] || temp[0] > 360) throw ''
                  if (Number(temp[1]) != temp[1] || temp[0] > 100) throw ''

                  h = temp[0]
                  s = l = temp[1]

                } else if (temp.length >= 3) {
                  if (Number(temp[0]) != temp[0] || temp[0] > 360) throw ''
                  if (Number(temp[1]) != temp[1] || temp[0] > 100) throw ''
                  if (Number(temp[2]) != temp[2] || temp[0] > 100) throw ''

                  h = temp[0]
                  s = temp[1]
                  l = temp[2]
                }

                msg_ret[key].offset.H = h
                msg_ret[key].offset.S = s
                msg_ret[key].offset.L = l

                msg_ret[key].msg = `H: ${h}, S: ${s}, L: ${l}`
              } catch {
                msg_ret[key].msg = '不合法的数据'
              }
            }"></input>
            <i>{{ msg_ret[key].msg }}</i>
          </div>
        </div>
      </li>
    </ul>
  </div>

  <contextmenu :ctmu="ctxMenu_output" :key="key" />
  <contextmenu :ctmu="ctxMenu_resultRender" />
  <contextmenu :ctmu="ctxMenu_global" />
</template>

<script setup>


import { ref, shallowRef, reactive, onMounted, nextTick, watch, onUpdated, computed } from 'vue'
import { msg, downloadJSON } from '@/modules/fns'
import { analyzeImageEmotions } from '@/assets/lib/utils'
import { base64ToBlobURL } from '@/assets/lib/function.lib'
import { ws } from "@/modules/websocket"
import { HEX2RGB, RGB2HSL, HSL2RGB } from '@/modules/colorAdjuster'

import html2canvas from 'html2canvas'

import { toggleIcon } from '@/assets/data/contextmenu_list'
import contextmenu from '@/assets/components/ContextMenu.vue'

const calc_conf = ref({
  k: 12,
  min_px: 100,
  color_thresh: 20
})
const img_ret = ref([])
const msg_ret = ref(null)

const ctxMenu_global = shallowRef({})

const ipt_getImgPath = ref(null)
const imgPath = ref(null)
const img_metadata = ref(null)

const doing = ref({
  calc: false,
  getImg: false
})

const global_tmp = {
  retRender: {}
}

const scaleRate = ref(.02)

// 像素容器
const ptWrapper = ref(null)
const ptWrapperSize = ref(90)
const pxCtr = ref(null)
const ptMain = ref(null)
// 渲染结果 图片
const pxResult = ref('')
const pxRet_auto = ref(true)
const pxRetSize = ref(15)

const pxIdle = ref(true)

const portIpt = ref(null)

// WebSocket server
const websocket = ref(null)
const wss = ref(null)

const key = ref(-100)
// 动画样式等
const individual = ref(true)

// 低配win10日历悬停效果
const followCursor = ref(null)
const followCursor_size = ref(0)
const followCursor_pos = ref([0, 0])
const followCursor_style = ref([`opacity: 0;transition: all 0s;`, `opacity: 1;transition: all .6s;`])
followCursor_style.value[2] = followCursor_style.value[0]

const pxData = ref({})
const maskData_origin = shallowRef({})
const maskData = reactive({
  opacity: .8,
  change: {
    H: 0,
    S: 0,
    L: 0
  },
  data: null
})

const rangeConf = reactive({
  random: {
    H: 24,
    S: 12,
    L: 12
  },
  err: null

})

// 画布大小 单位 *100%
const pxcs = ref({
  width: .9,
  // 后续计算
  height: 0
})

const pageSize = ref([])
const pxcsr = ref({})

function set_FlowCur_size() {
  followCursor_size.value = 1080 * ptWrapperSize.value / 100
}

function set_flowCur(event) {
  const x = event.pageX
  const y = event.pageY

  const parPos = ptWrapper.value.getBoundingClientRect()


  followCursor_pos.value[0] = x - parPos.left - followCursor_size.value / 2 + 'px'
  followCursor_pos.value[1] = y - parPos.top - followCursor_size.value / 2 + 'px'
}

watch(maskData, () => {
  setTimeout(resultRender, 1)
})

watch(imgPath, (newVal, oldVal) => {
  if (newVal == 'null') imgPath.value = null

  if (imgPath.value) {
    global_tmp.getImgInfoTimer = setInterval(() => {
      if (wss.value) {
        wss.value({
          req: 'get-img-info',
          path: imgPath.value,
          token: Date.now()
        })

        clearInterval(global_tmp.getImgInfoTimer)
        global_tmp.getImgInfoTimer = null
        return
      }
    }, 500)
  }
})

onMounted(() => {
  set_FlowCur_size()

  msg('**一切**<font color=green>**正常**</font><font color=#5d9fd1>~~~</font>')

  connect(localStorage.getItem('last_ws_port') || 11111)


  imgPath.value = localStorage.getItem('last-img-path')


  nextTick(() => {
    triggerWinResize()
  })

  window.addEventListener('resize', e => {
    //#region 确保横纵比
    let w = document.body.clientWidth,
      h = document.body.clientHeight
    pageSize.value = [w, h]

    pxcsr.value = {
      wr: pxcs.value.width * w,
      hr: pxcs.value.width * w * pxcs.value.height / pxcs.value.width
    }
    //#endregion
  })
})

// 触发页面缩放
function triggerWinResize() {
  // 创建并派发resize事件
  const resizeEvent = new Event('resize')
  window.dispatchEvent(resizeEvent)
}

//#region websock连接
// 连接ws
const tokenSet = {}
let pxSetTemp = []
function connect(port) {
  let ws_port = port ?? portIpt.value.value

  try {
    ws(ws_port, { req: 'check' }, (smsg, socket) => {

      // console.log(smsg)
      switch (smsg.header) {
        case 'check':
          if (smsg.code == 0 && smsg.data == 'ok') {
            localStorage.setItem('last_ws_port', ws_port)

            websocket.value = socket
            wss.value = socket.sender

            // 反向赋值
            portIpt.value.value = ws_port

            msg({
              text: `成功连接 \`Websocket\` 服务器，端口:<font color="#61afef">${ws_port}</font>`,
              delay: 5000
            })
          } else if (smsg.code == 1 && smsg.data == 'bye') {
            msg('socket服务关闭')
          } else {
            throw socket.msg.code
          }
          break;

        // 逐行获取图片像素
        case 'get-img-line':
          const imgInfo = smsg.data

          doing.value.getImg.setText(`图形数据加载进度${(imgInfo.rowIndex + 1) / imgInfo.height * 100}%`)

          const w = imgInfo.width, h = imgInfo.height

          if (!pxSetTemp.length) {
            pxSetTemp = Array.from({ length: h }, () => new Array(w))
          }

          if (!tokenSet[smsg.header] || tokenSet[smsg.header] == imgInfo.token) {
            pxSetTemp[imgInfo.rowIndex] = imgInfo.row.map(hex => RGB2HSL(HEX2RGB(hex)))

            if (imgInfo.rowIndex + 1 == imgInfo.height) {
              msg(imgInfo.path + ' 加载完成')

              for (let i = 0; i < imgInfo.height; i++) {
                pxData.value.paint = pxSetTemp
              }

              pxcs.value.height = h / w * pxcs.value.width

              pxData.value.s = [w, h]

              // 触发容器大小修改
              nextTick(triggerWinResize)

              setTimeout(resultRender, 1)

              // 
              let emotion = analyzeImageEmotions(pxData.value.paint)
              console.log(emotion)
              msg({
                text: `**<font color=#61afef>${emotion.join('、')}</font>**`,
                title: '情感色彩',
                delay: 12000
              })

              // 清空缓存
              pxSetTemp.value = null

              // 设置状态
              doing.value.getImg.del(() => { doing.value.getImg = null })
            }
          } else {

          }

          break;

        case 'get-img-info':
          console.log(smsg.data);

          img_metadata.value = smsg.data

          break
        case 'select-file':
          imgPath.value = smsg.data

          localStorage.setItem('last-img-path', smsg.data)

          break

        case 'calc-stru':
          console.log(smsg.data)
          // 仅显示首行
          msg('接收到命令行回复：' + smsg.data.data.replace('\n', ''))
          break
        case 'calc-split':
          const data = JSON.parse(smsg.data.data)

          img_ret.value = []
          msg_ret.value = data
          join(data)
          break
        default:
          break;
      }


    })
  } catch (err) {
    msg({
      title: '出错了',
      text: '<font color=red>连接到 **`WebSocket服务器`** 时出错:</font>' + err,
      delay: 5000
    })
  }
}

// 
function join(data) {
  console.log(data);


  Object.values(data).forEach(e => {
    e.data.forEach((a, i) => {
      if (!img_ret.value[i]) img_ret.value[i] = []

      a.forEach((b, j) => {

        if (b == 1) {
          img_ret.value[i][j] = HSL2RGB({
            H: e.split_color.H + e.offset.H,
            S: e.split_color.S + e.offset.S,
            L: e.split_color.L + e.offset.L
          })
        } else if (!img_ret.value[i][j]) {
          img_ret.value[i][j] = {
            R: 255,
            G: 255,
            B: 255
          }
        }

      })
    })
  })

  img_ret.value = rgb2dArrayToBase64(img_ret.value)
}

function rgb2dArrayToBase64(rgb2dArray) {
  // 创建离屏 Canvas
  const canvas = document.createElement('canvas');
  const height = rgb2dArray.length;
  const width = rgb2dArray[0]?.length || 0;

  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');

  // 创建 ImageData 对象
  const imageData = ctx.createImageData(width, height);
  const data = imageData.data;

  // 填充像素数据
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const pixel = rgb2dArray[y][x];
      const index = (y * width + x) * 4;

      data[index] = pixel.R;     // 红色通道 (0-255)
      data[index + 1] = pixel.G;  // 绿色通道 (0-255)
      data[index + 2] = pixel.B;  // 蓝色通道 (0-255)
      data[index + 3] = 255;      // Alpha 通道 (255 = 不透明)
    }
  }

  // 将图像数据绘制到 Canvas
  ctx.putImageData(imageData, 0, 0);

  // 转换为 Base64
  return canvas.toDataURL('image/png');
}

// 重点计算部分
function calc() {
  // doing.value.calc = msg('开始计算色块结构', true)
  doing.value.calc = true



  const pxd = JSON.parse(JSON.stringify(pxData.value.paint))

  wss.value({
    req: 'calc-stru',
    data: JSON.stringify(pxData.value.paint),
    token: Date.now()
  })

  const groups = {}

  const tol = colorCountConf.value.tolerance.hsl

  // 两色差距是否处在预定容差内
  const isWithin = (hsl1, hsl2) => {
    try {
      if (Math.abs(hsl1.H - hsl2.H) > tol.H) throw ''
      if (Math.abs(hsl1.S - hsl2.S) > tol.S) throw ''
      if (Math.abs(hsl1.L - hsl2.L) > tol.L) throw ''
    } catch (err) {

      if (err == '') return false
      console.error(err)
    }

    return true
  }

  // 结构优化
  pxd.forEach((row, i) => {
    row.forEach((col, j) => {
      delete col.val
      delete col.HSL_Rate.val

      pxd[i][j] = {
        H: col.HSL_Rate.H,
        S: col.HSL_Rate.S * 100,
        L: col.HSL_Rate.L * 100,
        group: JSON.stringify(col.HSL_Rate)
      }

    })
  })

  msg('结构重构完成')
    .then(() => calcColorGroup())

  // 分步
  msg('色彩统计完成')
    .then(() => calcStru())

  function calcColorGroup() {
    // 色块分组
    pxd.forEach((row, i) => {
      row.forEach((col, j) => {

        try {
          let rest = {
            x: pxd.length - i - 1,
            y: row.length - j - 1
          }

          for (let _i = 0; _i < rest.x; _i++) {
            for (let _j = 0; _j < rest.y; _j++) {
              const pxc = pxd[_i + i][_j + j]

              if (isWithin(pxd[i][j], pxc)) {
                pxd[_i + i][_j + j].group = pxd[i][j].group
              }
              // }
            }
          }
        } catch { }
      })
    })
  }

  function calcStru() {
    // 整合同组色块数据
    JSON.parse(JSON.stringify(pxd)).forEach((row, i) => {
      row.forEach((col, j) => {
        let g = col.group
        delete col.group

        if (!groups[g]) {
          groups[g] = {
            hsl: [],
            pos: []
          }
        }

        groups[g].hsl.push(col)
        groups[g].pos.push({
          x: j,
          y: i
        })
      })
    })

    // 计算均值
    const calcMean = arr => arr.reduce((sum, val) => sum + val, 0) / arr.length

    // 计算中位数
    const calcMedian = arr => {
      // sort 会变更原数组
      const sorted = [...arr].sort((a, b) => a - b)
      const mid = Math.floor(sorted.length / 2)
      return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]
    }

    Object.keys(groups).forEach(e => {

      let hArr = groups[e].hsl.map(e => e.H)
      let sArr = groups[e].hsl.map(e => e.S)
      let lArr = groups[e].hsl.map(e => e.L)

      // 中位数
      groups[e].median = {
        H: calcMedian(hArr),
        S: calcMedian(sArr),
        L: calcMedian(lArr)
      }

      // 均值
      groups[e].mean = {
        H: calcMean(hArr),
        S: calcMean(sArr),
        L: calcMean(lArr)
      }
    })

    console.log(groups)


    // 
    const maskData_temp = Array.from({ length: pxData.value.s[1] }, () => new Array(pxData.value.s[0]))
    Object.keys(groups).forEach(e => {
      // 去除单像素成组
      // if (groups[e].hsl.length == 1) {
      //   delete groups[e]
      // } else {

      // 生成可视化遮罩
      groups[e].hsl.forEach((_, i) => {
        let pos = groups[e].pos[i]

        switch (Number(colorCountConf.value.mode)) {
          case 1:
            // 中位数            
            // maskData_temp[pos.y][pos.x] = `HSL(${groups[e].median.H}, ${groups[e].median.S}%, ${groups[e].median.L}%)`
            maskData_temp[pos.y][pos.x] = [groups[e].median.H, groups[e].median.S, groups[e].median.L]
            break;
          case 2:
            // 均值
            // maskData_temp[pos.y][pos.x] = `HSL(${groups[e].mean.H}, ${groups[e].mean.S}%, ${groups[e].mean.L}%)`
            maskData_temp[pos.y][pos.x] = [groups[e].mean.H, groups[e].mean.S, groups[e].mean.L]
            break;

          default:
            // 最左上角
            let hsl = JSON.parse(e)
            // maskData_temp[pos.y][pos.x] = `HSL(${hsl.H}, ${hsl.S * 100}%, ${hsl.L * 100}%)`
            maskData_temp[pos.y][pos.x] = [hsl.H, hsl.S * 100, hsl.L * 100]
            break;
        }
      })
      // }

    })

    console.log(maskData_temp);
    maskData.data = maskData_temp
    maskData_origin.value = JSON.parse(JSON.stringify(maskData.data))


    console.log(pxd)

    console.log(groups)

    console.log(Object.keys(groups).length)

    console.log(maskData);


    colorCountConf.value.groups = groups


    doing.value.calc = false
    msg('色块计算完成')
    setTimeout(resultRender, 1)
  }
}

// 断开ws连接
function disconnect() {
  websocket.value.close()
  websocket.value = null
}
//#endregion

function setPtWrapperSize(val) {
  pxcs.value.width = val / 100
  pxcs.value.height = pxcs.value.width * pxData.value.s[1] / pxData.value.s[0]
  ptWrapperSize.value = val
  triggerWinResize()
  set_FlowCur_size()
}

// 
function resultRender() {
  if (!pxRet_auto.value) return

  const options = {
    useCORS: true,       // 允许加载跨域图片
    scale: 2,            // 提高输出分辨率（默认1）
    backgroundColor: "#fff"
  }
  // followCursor.value.style.display = 'none'
  pxIdle.value = false
  clearTimeout(global_tmp.retRender.timer1)
  clearTimeout(global_tmp.retRender.timer2)
  global_tmp.retRender.timer1 = setTimeout(() => html2canvas(pxCtr.value, options).then(canvas => {
    const base64 = canvas.toDataURL("image/png")

    pxResult.value = base64ToBlobURL(base64)
    global_tmp.retRender.timer2 = setTimeout(() => {
      pxIdle.value = true
    }, 50)

    // followCursor.value.style.display = 'block'
  }), 100)
}


//#region 右键菜单
const ctxMenu_output = computed(() => ({
  list: [{
    text: '输出结果',
    type: 'title',
    drag: true
  }, {
    type: 'splitline'
  }, {
    type: 'seekbar',
    clickDispear: false,
    text: '画布大小',
    seekbar: {
      pos: ptWrapperSize.value,
      min: 10,
      max: 100,
      interval: 5
    },
    fn: elem => {
      elem.on('drag', () => setPtWrapperSize(elem.val))
    }
  }, {
    type: 'seekbar',
    clickDispear: false,
    text: '遮罩',
    seekbar: {
      pos: maskData.opacity,
      min: 0,
      max: 1,
      interval: 0.01
    },
    fn: elem => {
      elem.on('drag', () => {
        maskData.opacity = elem.val
      })
    }
  }, {
    type: 'splitline'
  }, {
    type: 'option',
    clickDispear: false,
    text: '随机生成',
    fn: elem => {
      elem.on('click', () => {
        const rd = num => {
          return Math.random() * num * (Math.round(Math.random()) ? 1 : -1)
        }

        elem.getGroup('色相').set_seekbar(rd(rangeConf.random.H)).apply()
        elem.getGroup('彩度').set_seekbar(rd(rangeConf.random.S)).apply()
        elem.getGroup('亮度').set_seekbar(rd(rangeConf.random.L)).apply()
      })
    }
  }, {
    type: 'option',
    clickDispear: false,
    text: '重置',
    fn: elem => {
      elem.on('click', () => {
        rangeConf.random = {
          H: 0,
          S: 0,
          L: 0
        }

        maskData.change = rangeConf.random
        key.value++
      })
    }
  }, {
    type: 'splitline'
  }, {
    text: '全局调整',
    type: 'subtitle',
    drag: true
  }, {
    type: 'seekbar',
    clickDispear: false,
    text: '色相',
    seekbar: {
      pos: maskData.change.H,
      min: 0,
      max: 360,
      interval: 5
    },
    oid: '色相',
    fn: elem => {
      elem.on('drag-done', () => {
        if (maskData.data) {
          maskData.change.H = elem.val

          console.log(elem);


        } else {
          msg('结构尚未计算！')
        }
      })
    }
  }, {
    type: 'seekbar',
    clickDispear: false,
    text: '彩度',
    seekbar: {
      pos: 0,
      interval: 0.1
    },
    fn: elem => {
      elem.on('drag-done', () => {
        if (maskData.data) {

          maskData.change.S = elem.val
        } else {
          msg('结构尚未计算！')
        }
      })
    }
  }, {
    type: 'seekbar',
    clickDispear: false,
    text: '亮度',
    seekbar: {
      pos: 0,
      interval: 0.1
    },
    fn: elem => {
      elem.on('drag-done', () => {
        if (maskData.data) {
          maskData.change.L = elem.val
        } else {
          msg('结构尚未计算！')
        }
      })
    }
  }],
  size: '20rem',
  elem: '#px-container'
}))

const ctxMenu_resultRender = computed(() => ({
  list: [{
    text: '渲染结果',
    type: 'title',
    drag: true
  }, {
    type: 'splitline'
  }, {
    text: '查看大图',
    type: 'option',
    fn: elem => {
      elem.on('click', () => {
        window.open(pxResult.value, "_blank")
      })
    }
  }, {
    type: 'seekbar',
    clickDispear: false,
    text: '显示大小',
    seekbar: {
      pos: pxRetSize.value,
      interval: .5,
      min: 5,
      max: 20
    },
    fn: elem => {
      elem.on('drag', () => {
        pxRetSize.value = elem.val
      })
    }
  }],
  elem: '#result-render'
}))

ctxMenu_global.value = {
  list: [{
    text: '总背景',
    type: 'title',
    drag: true
  }, {
    type: 'splitline'
  }, {
    text: '查看开源地址',
    type: 'link',
    key: ['ctrl', 'shift', 'o'],
    href: 'https://gitee.com/u1iz/Multidimensional-Emotion-Semantic-Color'
  }],
  elem: '#app',
  size: '15rem'
}

// 
const colorCountConf = ref({
  err: false,
  mode: 1,
  // 容差
  tolerance: {
    // 颜色
    /* hex: 12, */
    hsl: {
      H: 24,
      S: 6,
      L: 6
    },
    // 位置 *100%
    /* positon: .1 */
  },
  groups: null
  // 总计数
  // maxLength: 10
})
//#endregion

</script>

<style scoped>
@import url("./assets/app.css");
</style>